package com.wechat.page.chat.list;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;

import com.swak.entity.YesNo;
import com.swak.fx.support.AbstractPage;
import com.swak.fx.support.FXMLView;
import com.swak.fx.support.ui.UI;
import com.swak.fx.support.ui.UIScrollPane;
import com.swak.ui.Event;
import com.swak.ui.EventListener;
import com.swak.utils.Maps;
import com.swak.utils.StringUtils;
import com.swak.utils.time.DateTimes;
import com.wechat.Cons;
import com.wechat.dto.ChatDTO;
import com.wechat.dto.MessageDTO;
import com.wechat.page.api.ChatApi;
import com.wechat.page.chat.message.MessageThumbComp;

import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;

/**
 * ListPage.
 * 
 * @author 618lf
 */
@FXMLView(title = "聊天列表", value = "ListPage.fxml", css = "ListPage.css")
public class ListPage extends AbstractPage implements EventListener {

	private VBox vbox;
	private UIScrollPane root;

	@Autowired
	private ChatApi chatApi;

	public ListPage() {
		this.vbox = new VBox();
		this.root = (UIScrollPane) super.root;
	}

	@Override
	public void initializeParts() {
		super.initializeParts();
		this.vbox.setAlignment(Pos.CENTER);
		this.vbox.getStyleClass().add("list-page__inner");
		this.root.setContent(vbox);
		this.root.setFitToWidth(true);
		this.root.setHbarPolicy(ScrollBarPolicy.NEVER);
		this.root.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
	}

	@Override
	public void setupEventHandlers() {
		super.setupEventHandlers();
	}

	@Override
	public void setupBindings() {
		super.setupBindings();
		this.activedProperty.bind(Bindings.createStringBinding(() -> {
			ChatDTO active = this.activeProperty().get();
			return active != null ? active.getId() : StringUtils.EMPTY;
		}, this.activeProperty()));
	}

	@Override
	public void setupValueChangedListeners() {
		super.setupValueChangedListeners();
		this.chatsProperty().addListener(this::onChatsChange);
		this.activedProperty.addListener(this::onChatActive);
	}

	private void onChatActive(ObservableValue<? extends String> ob, String unActive, String active) {
		if (unActive != null) {
			Node remove = this.findChatNode(ChatDTO.of().setId(unActive));
			if (remove != null) {
				remove.getStyleClass().remove("active");
			}
		}
		if (active != null) {
			Node remove = this.findChatNode(ChatDTO.of().setId(active));
			if (remove != null) {
				remove.getStyleClass().add("active");
				ChatDTO activeChat = (ChatDTO) remove.getProperties().get(UI.Conts.Data_Key);
				if (activeChat != null && activeChat.getUnReadCount() != null && activeChat.getUnReadCount() > 0) {
					this.doChatActive(activeChat);
				}
			}
		}
	}

	private void doChatActive(ChatDTO active) {
		this.$run(() -> {
			this.chatApi.updateFlag(ChatDTO.of().setId(active.getId()).setUnReadCount(0)).thenAccept(chat -> {
				this.$runOnUI(() -> {

					// 发送接收事件
					if (LOGGER.isDebugEnabled()) {
						LOGGER.debug("[聊天菜单] - 发送事件 ：{}", Cons.Event_Msg_Recv);
					}
					this.$post(Event.of().setKey(Cons.Event_Msg_Recv));

					// 发送刷新
					if (LOGGER.isDebugEnabled()) {
						LOGGER.debug("[聊天菜单] - 发送事件 ：{}", Cons.Event_Chat_Reload);
					}
					this.$post(Event.of().setKey(Cons.Event_Chat_Reload).setMessage(chat));

				});
			});
		});
	}

	@SuppressWarnings("unchecked")
	private void onChatsChange(Change<? extends ChatDTO> c) {
		while (c.next()) {
			if (c.wasAdded()) {
				List<ChatDTO> adds = (List<ChatDTO>) c.getAddedSubList();
				for (ChatDTO chat : adds) {
					this.addChat(chat);
				}
			} else if (c.wasRemoved()) {
				List<ChatDTO> adds = (List<ChatDTO>) c.getRemoved();
				for (ChatDTO chat : adds) {
					this.removeChat(chat);
				}
			}
		}
	}

	private void addChat(ChatDTO chat) {

		// 只能添加一次
		if (this.hasChat(chat)) {
			return;
		}

		// 实例化界面
		StackPane inner = new StackPane();
		inner.getStyleClass().add("list-page__ph");
		MessageThumbComp messageThumb = MessageThumbComp.New(MessageThumbComp.class, this);
		messageThumb.activeProperty().bind(this.activeProperty());
		inner.getChildren().add(messageThumb.getRoot());
		inner.getProperties().put(UI.Conts.ID_Key, chat.getId());
		inner.getProperties().put(UI.Conts.Data_Key, chat);
		inner.getProperties().put(UI.Conts.Comp_Key, messageThumb);

		// 保存列表实例
		this.saveChatNode(chat, inner);

		// 添加到列表
		this.vbox.getChildren().add(inner);

		// 设置 Chat
		this.$runOnUI(() -> {
			messageThumb.chatProperty().set(chat);
		});
	}

	private void removeChat(ChatDTO chat) {
		StackPane node = this.findChatNode(chat);
		if (node != null) {
			MessageThumbComp messageThumb = (MessageThumbComp) node.getProperties().get(UI.Conts.Comp_Key);
			messageThumb.destory();
			this.vbox.getChildren().remove(node);
		}
	}

	// ================================================================================
	// Chat <==> Pane
	// ================================================================================
	private Map<String, StackPane> chatPanes = Maps.newHashMap();

	private synchronized boolean hasChat(ChatDTO chat) {
		return chatPanes.containsKey(chat.getId());
	}

	private synchronized void saveChatNode(ChatDTO chat, StackPane inner) {
		this.chatPanes.put(chat.getId(), inner);
	}

	private synchronized StackPane findChatNode(ChatDTO chat) {
		return chat != null ? this.chatPanes.get(chat.getId()) : null;
	}

	// ================================================================================
	// 异步加载数据
	// ================================================================================
	@Override
	public void mounted() {
		super.mounted();

		this.$run(() -> this.load());
	}

	private void load() {
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("[聊天列表] - 准备加载数据");
		}
		this.chatApi.chats().thenAccept(chats -> {
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("[聊天列表] - 加载数据:{}", chats.size());
			}
			this.$runUI(() -> this.chatsProperty().addAll(chats));
		});
	}

	// ================================================================================
	// 事件监听
	// ================================================================================
	@Override
	public void listen(Event event) {

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("[聊天列表] - 收到事件 ：{}", event);
		}

		// 刷新
		if (Cons.Event_Chats_Refresh.equals(event.getKey())) {
			this.$runUI(() -> this.onRefresh());
		} else if (Cons.Event_Chat_Select.equals(event.getKey())) {
			this.$runUI(() -> this.onChatSelect(event.getMessage()));
		} else if (Cons.Event_Chat_Reload.equals(event.getKey())) {
			this.$runUI(() -> this.onChatReload(event.getMessage()));
		} else if (Cons.Event_Chat_Refresh.equals(event.getKey())) {
			this.$runUI(() -> this.onChatRefresh(event.getMessage()));
		} else if (Cons.Event_Msg_Send.equals(event.getKey())) {
			this.$runUI(() -> this.onMessageSend(event.getMessage()));
		} else if (Cons.Event_Msg_Recv.equals(event.getKey())) {
			this.$runUI(() -> this.onMessageRecv(event.getMessage()));
		}
	}

	private void onChatSelect(ChatDTO select) {
		StackPane node = this.findChatNode(select);
		if (node == null) {
			this.chatApi.create(select).thenAccept(c -> {
				this.$runOnUI(() -> {
					this.addChat(c);
					this.onChatSort(c);
					this.onMessageRefresh(c);
					this.activeProperty().set(c);
				});
			});
		} else {
			ChatDTO chat = node != null ? ((ChatDTO) node.getProperties().get(UI.Conts.Data_Key)) : ChatDTO.of();
			this.$runOnUI(() -> {
				this.onChatSort(chat);
				this.onMessageRefresh(chat);
				this.activeProperty().set(chat);
			});
		}
	}

	private void onChatReload(ChatDTO chat) {
		StackPane node = this.findChatNode(chat);
		if (node != null) {
			this.chatApi.get(chat.getId()).thenAccept(c -> {
				this.$runOnUI(() -> {
					this.doChatReload(c);
				});
			});
		}
	}

	private void doChatReload(ChatDTO reload) {

		// copy some attr
		StackPane node = this.findChatNode(reload);
		ChatDTO chat = node != null ? ((ChatDTO) node.getProperties().get(UI.Conts.Data_Key)) : ChatDTO.of();
		reload.setDraft(chat.getDraft());

		// check active chat
		ChatDTO activeChat = this.activeProperty().get();
		if (activeChat != null && activeChat.getId().equals(reload.getId())) {
			this.activeProperty().set(reload);
		}

		// Refresh chat
		this.onChatRefresh(reload);

		// sort chat
		this.onChatSort(reload);
	}

	private void onChatRefresh(ChatDTO chat) {
		this.$runOnUI(() -> {
			this.doChatRefresh(chat);
			this.onMessageRefresh(chat);
		});
	}

	private void doChatRefresh(ChatDTO refresh) {
		StackPane node = this.findChatNode(refresh);
		node.getProperties().put(UI.Conts.Data_Key, refresh);
		MessageThumbComp messageThumb = (MessageThumbComp) node.getProperties().get(UI.Conts.Comp_Key);
		messageThumb.chatProperty().set(refresh);
	}

	private void onMessageRefresh(ChatDTO refresh) {
		StackPane node = this.findChatNode(refresh);
		MessageThumbComp messageThumb = (MessageThumbComp) node.getProperties().get(UI.Conts.Comp_Key);
		messageThumb.refreshProperty().set(DateTimes.nowMilli());
		this.refreshProperty().set(DateTimes.nowMilli());
	}

	private void onMessageSend(MessageDTO message) {
		StackPane node = this.findChatNode(ChatDTO.of().setId(message.getTalker()));
		ChatDTO chat = node != null ? ((ChatDTO) node.getProperties().get(UI.Conts.Data_Key)) : ChatDTO.of();
		chat.setId(message.getTalker()).setLastMsgContent(message.getMsgBrief()).setLastMsgDate(message.getCreateDate())
				.setLastMsgType(message.getMsgType()).setLastMsgId(message.getId())
				.setUpdateDate(message.getCreateDate());
		this.$runOnUI(() -> {
			this.onChatSort(chat);
			this.onMessageRefresh(chat);
		});
	}

	private void onMessageRecv(MessageDTO message) {
		StackPane node = this.findChatNode(ChatDTO.of().setId(message.getTalker()));
		ChatDTO chat = node != null ? ((ChatDTO) node.getProperties().get(UI.Conts.Data_Key)) : ChatDTO.of();
		chat.setId(message.getTalker()).setLastMsgContent(message.getMsgBrief()).setLastMsgDate(message.getCreateDate())
				.setLastMsgType(message.getMsgType()).setLastMsgId(message.getId())
				.setUpdateDate(message.getCreateDate());
		if (node == null) {
			this.chatApi.create(chat).thenAccept(c -> {
				this.$runOnUI(() -> {
					this.addChat(c);
					this.onChatSort(c);
					this.onMessageRefresh(c);
				});
			});
		} else {
			this.$runOnUI(() -> {
				this.onChatSort(chat);
				this.onMessageRefresh(chat);
			});
		}
	}

	private void onChatSort(ChatDTO chat) {

		// 查找自己所在的位置
		List<Node> nodes = this.vbox.getChildren();
		int index = -1, old = -1;
		for (int i = 0; i < nodes.size(); i++) {
			Node node = nodes.get(i);
			ChatDTO c = node != null ? ((ChatDTO) node.getProperties().get(UI.Conts.Data_Key)) : ChatDTO.of();
			if (c.getId().equals(chat.getId())) {
				old = i;
				continue;
			}
			int sort = this.compareChats(c, chat);
			if (sort == -1) {
				index = i;
				break;
			}
		}

		// 不需要排序: 当前本身排在前面则不需要排序了
		if (old != -1 && (old == index - 1)) {
			return;
		}

		// 删除所在位置
		StackPane me = this.findChatNode(chat);
		this.vbox.getChildren().remove(me);

		// 添加到最后
		if (index == -1) {
			this.vbox.getChildren().add(me);
		} else {
			this.vbox.getChildren().add(index, me);
		}
	}

	private int compareChats(ChatDTO one, ChatDTO two) {
		if (one.getId().equals(two.getId())) {
			return 0;
		}

		// isTop
		if (one.getIsTop() != null && one.getIsTop() == YesNo.Yes.getType()
				&& !(two.getIsTop() != null && two.getIsTop() == YesNo.Yes.getType())) {
			return 1;
		}
		if (!(one.getIsTop() != null && one.getIsTop() == YesNo.Yes.getType())
				&& (two.getIsTop() != null && two.getIsTop() == YesNo.Yes.getType())) {
			return -1;
		}

		// updateDate
		if (one.getLastMsgDate().isAfter(two.getLastMsgDate())) {
			return 1;
		}
		if (one.getLastMsgDate().isBefore(two.getLastMsgDate())) {
			return -1;
		}

		// chat order
		return one.getChatOrder().compareTo(two.getChatOrder());
	}

	private void onRefresh() {
		this.chatsProperty().clear();
		this.$run(() -> this.load());
	}

	// ================================================================================
	// Recyclable
	// ================================================================================
	protected void setupRecyclableChatList() {
		this.root.vvalueProperty().addListener((ob, o, n) -> {
			this.layoutChatList();
		});
		this.root.viewportBoundsProperty().addListener((ob, o, n) -> {
			this.layoutChatList();
		});
	}

	protected void layoutChatList() {
		Node node = this.root.getContent();
		Bounds rootBounds = this.root.localToScene(this.root.getBoundsInLocal());
		if (node instanceof Pane) {
			ObservableList<Node> children = ((Pane) node).getChildren();
			for (int i = 0; i < children.size(); i++) {
				Node child = children.get(i);
				Bounds childbounds = child.localToScene(child.getBoundsInLocal());
				if (childbounds.getMinY() > rootBounds.getMaxY() || childbounds.getMaxY() < rootBounds.getMinY()) {
					child.setVisible(false);
				} else {
					child.setVisible(true);
				}
			}
		}
	}

	// ================================================================================
	// Properties
	// ================================================================================
	private final ListProperty<ChatDTO> chatsProperty = new SimpleListProperty<ChatDTO>(
			FXCollections.observableArrayList());

	public ListProperty<ChatDTO> chatsProperty() {
		return chatsProperty;
	}

	private final ObjectProperty<ChatDTO> activeProperty = new SimpleObjectProperty<>();

	public ObjectProperty<ChatDTO> activeProperty() {
		return activeProperty;
	}

	private final StringProperty activedProperty = new SimpleStringProperty();
	private final LongProperty refreshProperty = new SimpleLongProperty();

	public LongProperty refreshProperty() {
		return refreshProperty;
	}
}
